package gosu;

import javax.swing.JFrame;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Cursor;
import java.awt.Toolkit;
import java.awt.Point;
import java.awt.MouseInfo;
import java.awt.event.InputEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.PriorityQueue;
import java.util.Set;
import java.util.HashSet;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentLinkedQueue;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCapabilities;
import javax.media.opengl.GLCanvas;
import javax.media.opengl.GLDrawableFactory;
import javax.media.opengl.GL;
import javax.media.opengl.GLException;
import javax.media.opengl.glu.GLU;


//TODO: Think about breaking up this class
public class Window {
  private WindowFrame _frame;
  private double _updateInterval;
  private boolean _isFullscreen;
  private boolean _showing;
  private PriorityQueue<DrawOp> _drawOps;
  private GLCanvas _canvas;
  private ConcurrentLinkedQueue<InputEvent> _eventQueue;
  private Set<Button> _pressedButtons;
  private int _width;
  private int _height;
  private TextInput _textInput;
  private Point _currentMouseLoc; // the position of the mouse in the current game cycle

  private boolean _clipping;
  private int _clipX;
  private int _clipY;
  private int _clipWidth;
  private int _clipHeight;


  public Window(int width, int height, boolean fullscreen) {
    this(width, height, fullscreen, 60);
  }


  public Window(int width, int height, boolean fullscreen, int fps) {
    _updateInterval = 1000.0 / (double) fps;
    _width = width;
    _height = height;
    _isFullscreen = fullscreen;
    _frame = new WindowFrame(this);
    _frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    _frame.getContentPane().setPreferredSize(new Dimension(width, height));
    _frame.setBackground(java.awt.Color.black);

    _canvas = new GLCanvas(new GLCapabilities());
    _canvas.addGLEventListener(_frame);
    _canvas.setFocusTraversalKeysEnabled(false); // don't usurp the tab key
    _frame.add(_canvas, BorderLayout.CENTER);
    _drawOps = new PriorityQueue<DrawOp>();

    _eventQueue = new ConcurrentLinkedQueue<InputEvent>();
    _pressedButtons = new HashSet<Button>();

    hideCursor();

    // Make sure the GLCanvas has focus from the start
    _frame.addWindowListener(new WindowAdapter() {
      public void windowOpened(WindowEvent e) {
        _canvas.requestFocus();
      }
    });

    _canvas.addKeyListener(new KeyAdapter() {
      public void keyPressed(KeyEvent e) {
        _eventQueue.add(e);
      }

      public void keyReleased(KeyEvent e) {
        _eventQueue.add(e);
      }
    });

    _canvas.addMouseListener(new MouseAdapter() {
      public void mousePressed(MouseEvent e) {
        _eventQueue.add(e);
      }

      public void mouseReleased(MouseEvent e) {
        _eventQueue.add(e);
      }
    });
  }


  private void hideCursor() {
    Toolkit tk = Toolkit.getDefaultToolkit();
    java.awt.Image image = tk.createImage(new byte[]{0});
    Cursor noCursor = tk.createCustomCursor(image, new Point(1, 1), "blank");
    _frame.setCursor(noCursor);
  }


  /**
   * Starts the main event loop.
   */
  public void showWindow() {
    _showing = true;
    _frame.pack();
    _frame.setVisible(true);

    while (_showing) {
      long start = System.currentTimeMillis();

      // GLExceptions don't help us at all
      try {
        _canvas.display();
      } catch (GLException e) {
        if (e.getCause() instanceof RuntimeException) {
          throw (RuntimeException) e.getCause();
        }
        throw e;
      }

      long end = System.currentTimeMillis();

      if (end - start < _updateInterval) {
        try {
          Thread.sleep((long) _updateInterval - (end - start));
        } catch (InterruptedException e) { }
      }
    }

    _frame.setVisible(false);
    _frame.dispose();
  }


  /**
   * Closes the window if it is currently shown.
   */
  public void close() {
    _showing = false;
  }


  /**
   * Called every _updateInterval milliseconds while the window is being
   * shown. Your application's main game logic goes here.
   */
  public void update() {}


  /**
   * Called after every update and when the OS wants the window to
   * repaint itself. Your application's rendering code goes here.
   */
  public void draw() {}


  public void addDrawOp(DrawOp op) {
    if (_clipping) {
      op.clip(_clipX, _clipY, _clipWidth, _clipHeight);
    }
    _drawOps.add(op);
  }


  protected void processDrawOps(GL gl) {
    while (!_drawOps.isEmpty()) {
      DrawOp op = _drawOps.poll();

      if (op.isClipped()) {
        gl.glEnable(GL.GL_SCISSOR_TEST);
        gl.glScissor(op._clipX, op._clipY, op._clipWidth, op._clipHeight);
      }

      op.execute();

      if (op.isClipped()) {
        gl.glDisable(GL.GL_SCISSOR_TEST);
      }
    }
  }


  protected void processInput() {
    // reset the mouse location
    _currentMouseLoc = null;

    while (!_eventQueue.isEmpty()) {
      InputEvent event = _eventQueue.poll();
      Button button = Button.getButton(event);

      switch (event.getID()) {
        case MouseEvent.MOUSE_PRESSED:
        case KeyEvent.KEY_PRESSED:
          if (_textInput == null || !_textInput.feedEvent(event)) {
            _pressedButtons.add(button);
            buttonDown(button);
          }
          break;
        case MouseEvent.MOUSE_RELEASED:
        case KeyEvent.KEY_RELEASED:
          _pressedButtons.remove(button);
          buttonUp(button);
          break;
      }
    }
  }


  /**
   * Get the location of the mouse on the screen relative to the top left corner
   * of the window.
   */
  public Point getMouseLocation() {
    if (_currentMouseLoc == null) {
      Point mouse = MouseInfo.getPointerInfo().getLocation();
      Point window = _frame.isVisible() ? _canvas.getLocationOnScreen() : new Point(0, 0);
      _currentMouseLoc = new Point(mouse.x - window.x, mouse.y - window.y);
    }
    return _currentMouseLoc;
  }


  /**
   * Called before update when the user pressed a button while the 
   * window had the focus.
   */
  public void buttonDown(Button button) {}


  /**
   * Same as buttonDown. Called when the user released a button.
   */
  public void buttonUp(Button button) {}


  /**
   * Returns true if the button is currently pressed.
   */
  public boolean isButtonDown(Button button) {
    return _pressedButtons.contains(button);
  }


  public String caption() { return _frame.getTitle(); }


  public void setCaption(String caption) {
    _frame.setTitle(caption);
  }


  public void setTextInput(TextInput input) {
    _textInput = input;
  }


  public TextInput getTextInput() {
    return _textInput;
  }


  public void drawLine(double x1, double y1, Color c1, double x2, double y2, Color c2) {
    drawLine(x1, y1, c1, x2, y2, c2, 0, Image.DEFAULT);
  }


  public void drawLine(double x1, double y1, Color c1, double x2, double y2, Color c2, double z, int mode) {
    addDrawOp(new ImageDrawOp(x1, y1, c1, x2, y2, c2, z, mode));   
  }


  public void drawTriangle(double x1, double y1, Color c1,
                           double x2, double y2, Color c2,
                           double x3, double y3, Color c3) {
    drawTriangle(x1, y1, c1, x2, y2, c2, x3, y3, c3, 0, Image.DEFAULT);
  }


  public void drawTriangle(double x1, double y1, Color c1,
                           double x2, double y2, Color c2,
                           double x3, double y3, Color c3,
                           double z, int mode) {
    addDrawOp(new ImageDrawOp(x1, y1, c1, x2, y2, c2, x3, y3, c3, z, mode));
  }


  public void drawQuad(double x1, double y1, Color c1,
                       double x2, double y2, Color c2,
                       double x3, double y3, Color c3,
                       double x4, double y4, Color c4) {
    drawQuad(x1, y1, c1, x2, y2, c2, x3, y3, c3, x4, y4, c4, 0, Image.DEFAULT);
  }


  public void drawQuad(double x1, double y1, Color c1,
                       double x2, double y2, Color c2,
                       double x3, double y3, Color c3,
                       double x4, double y4, Color c4,
                       double z, int mode) {
    addDrawOp(new ImageDrawOp(x1, y1, c1, x2, y2, c2, x3, y3, c3, x4, y4, c4, z, mode));
  }


  public int getWidth() { 
    return _frame.isVisible() ? _frame.getWidth() : _width;
  }

  public int getHeight() { 
    return _frame.isVisible() ? _frame.getHeight() : _height;
  }


  public void beginClipping(int x, int y, int width, int height) {
    _clipping = true;
    _clipX = x;
    _clipY = (getHeight() - y) - height;
    _clipWidth = width;
    _clipHeight = height;
  }


  public void endClipping() { _clipping = false; }
}


class WindowFrame extends JFrame implements GLEventListener {
  private Window _window;
  private GLU _glu = new GLU();

  public WindowFrame(Window window) {
    _window = window;
  }


  public void init(GLAutoDrawable drawable) {
    GL gl = drawable.getGL();
    gl.glClearColor(0f, 0f, 0f, 0f);
    gl.glPointSize(4.0f);
    gl.glEnable(GL.GL_BLEND);
  }


  public void display(GLAutoDrawable drawable) {
    GL gl = drawable.getGL();
    gl.glClear(GL.GL_COLOR_BUFFER_BIT);

    _window.draw();
    _window.processDrawOps(gl);
    _window.processInput();
    _window.update();
  }


  public void displayChanged(GLAutoDrawable drawable, boolean modeChanged, boolean deviceChanged) {
  }


  public int getWidth() { return getContentPane().getWidth(); }
  public int getHeight() { return getContentPane().getHeight(); }


  public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
    GL gl = drawable.getGL();
    gl.glViewport(0, 0, width, height);
    gl.glMatrixMode(GL.GL_PROJECTION);
    gl.glLoadIdentity();
    _glu.gluOrtho2D(0, getContentPane().getWidth(), getContentPane().getHeight(), 0);
  }
}
